package com.stars.easyms.base.util;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.core.Conventions;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

/**
 * <p>className: BeanUtil</p>
 * <p>description: spring的bean工具类</p>
 *
 * @author guoguifang
 * @version 1.2.2
 * @date 2019-07-15 19:24
 */
public final class BeanUtil {

    private static final String CONFIGURATION_CLASS_FULL = "full";

    private static final String CONFIGURATION_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass");

    /**
     * 主动向Spring容器中注册bean
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param clazz              注册的bean的类
     * @param args               构造方法的必要参数，顺序和类型要求和clazz中定义的一致
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String beanName, Class<T> clazz, Object... args) {
        // 校验beanName是否存在
        T bean = checkBeanByBeanName(applicationContext, beanName, clazz);
        if (bean != null) {
            return bean;
        }

        // 若不包含beanName，则创建beanDefinition构建者，并按照顺序添加构造方法的参数
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        for (Object arg : args) {
            beanDefinitionBuilder.addConstructorArgValue(arg);
        }
        // 获取beanDefinition对象并注册
        BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
        BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
        beanFactory.registerBeanDefinition(beanName, beanDefinition);
        return applicationContext.getBean(beanName, clazz);
    }

    /**
     * 主动向Spring容器中注册bean，如果是动态class时使用，且为懒加载模式，只有不存在bean对象时才创建实例，优先使用新版模式，若新版方法不存在则使用旧版
     *
     * @param applicationContext    Spring上下文
     * @param beanName              beanName
     * @param classSupplier         bean类提供者(可实现动态获取bean的类)
     * @param instanceSupplier      bean对象提供者
     * @param factoryBeanName       工厂类beanName
     * @param factoryBeanMethodName 生成bean对象的工厂类方法名称
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String beanName, Supplier<Class<T>> classSupplier,
                                     Supplier<T> instanceSupplier, String factoryBeanName, String factoryBeanMethodName) {
        try {
            return registerBean(applicationContext, beanName, classSupplier.get(), instanceSupplier);
        } catch (Throwable t) {
            // 此处必须catch Throwable类型不能使用Exception类型，否则会注册失败
            return registerBean(applicationContext, beanName, classSupplier.get(), factoryBeanName, factoryBeanMethodName);
        }
    }

    /**
     * 主动向Spring容器中注册bean，懒加载模式，只有不存在bean对象时才创建实例，优先使用新版模式，若新版方法不存在则使用旧版
     *
     * @param applicationContext    Spring上下文
     * @param beanName              beanName
     * @param clazz                 注册的bean的类
     * @param instanceSupplier      bean对象提供者
     * @param factoryBeanName       工厂类beanName
     * @param factoryBeanMethodName 生成bean对象的工厂类方法名称
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String beanName, Class<T> clazz,
                                     Supplier<T> instanceSupplier, String factoryBeanName, String factoryBeanMethodName) {
        try {
            return registerBean(applicationContext, beanName, clazz, instanceSupplier);
        } catch (Throwable t) {
            // 此处必须catch Throwable类型不能使用Exception类型，否则会注册失败
            return registerBean(applicationContext, beanName, clazz, factoryBeanName, factoryBeanMethodName);
        }
    }

    /**
     * 主动向Spring容器中注册bean，如果是动态class时使用，且为懒加载模式，只有不存在bean对象时才创建实例
     * 注：该方法只支持spring5.0以上
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param classSupplier      bean类提供者(可实现动态获取bean的类)
     * @param instanceSupplier   bean对象提供者
     * @return 返回注册到容器中的bean对象
     */
    @SuppressWarnings("unchecked")
    public static Object registerBean(ConfigurableApplicationContext applicationContext, String beanName, Supplier<Class> classSupplier, Supplier<Object> instanceSupplier) {
        Class clazz = classSupplier.get();
        // 先按照beanName检查
        Object bean = checkBeanByBeanName(applicationContext, beanName, clazz);
        if (bean == null) {
            // 若不包含beanName，则创建beanDefinition构建者，并按照顺序添加构造方法的参数
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz, instanceSupplier);
            // 获取beanDefinition对象并注册
            BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
            BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
            beanFactory.registerBeanDefinition(beanName, beanDefinition);
        }
        return applicationContext.getBean(beanName, clazz);
    }

    /**
     * 主动向Spring容器中注册bean，懒加载模式，只有不存在bean对象时才创建实例
     * 注：该方法只支持spring5.0以上
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param clazz              注册的bean的类
     * @param instanceSupplier   bean对象提供者
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String beanName, Class<T> clazz, Supplier<T> instanceSupplier) {
        // 先按照beanName检查
        T bean = checkBeanByBeanName(applicationContext, beanName, clazz);
        if (bean == null) {
            // 若不包含beanName，则创建beanDefinition构建者，并按照顺序添加构造方法的参数
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz, instanceSupplier);
            // 获取beanDefinition对象并注册
            BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
            BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
            beanFactory.registerBeanDefinition(beanName, beanDefinition);
        }
        return applicationContext.getBean(beanName, clazz);
    }

    /**
     * 主动向Spring容器中注册bean，如果是动态class时使用，且为懒加载模式，只有不存在bean对象时才创建实例
     * 注：该方法可支持spring所有版本
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param classSupplier      bean类提供者(可实现动态获取bean的类)
     * @param factoryBeanName    工厂类beanName
     * @param factoryMethodName  生成bean对象的工厂类方法名称
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String beanName, Supplier<Class<T>> classSupplier, String factoryBeanName, String factoryMethodName) {
        return registerBean(applicationContext, beanName, classSupplier.get(), factoryBeanName, factoryMethodName);
    }

    /**
     * 主动向Spring容器中注册bean，懒加载模式，只有不存在bean对象时才创建实例
     * 注：该方法可支持spring所有版本
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param clazz              注册的bean的类
     * @param factoryBeanName    工厂类beanName
     * @param factoryMethodName  生成bean对象的工厂类方法名称
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String beanName, Class<T> clazz, String factoryBeanName, String factoryMethodName) {
        // 先按照beanName检查
        T bean = checkBeanByBeanName(applicationContext, beanName, clazz);
        if (bean == null) {
            // 若不包含beanName，则创建beanDefinition构建者，并按照顺序添加构造方法的参数
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
            // 获取beanDefinition对象并注册
            BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
            beanDefinition.setFactoryBeanName(factoryBeanName);
            beanDefinition.setFactoryMethodName(factoryMethodName);
            BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
            beanFactory.registerBeanDefinition(beanName, beanDefinition);
        }
        return applicationContext.getBean(beanName, clazz);
    }

    /**
     * 主动向Spring容器中注册单例bean
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param instance           注册的bean对象
     */
    public static void registerSingleton(ConfigurableApplicationContext applicationContext, String beanName, Object instance) {
        Object bean = checkBeanByBeanName(applicationContext, beanName, instance.getClass());
        if (bean == null) {
            DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
            defaultListableBeanFactory.registerSingleton(beanName, instance);
        }
    }

    /**
     * 注册增加了Configuration注解的bean，并注册其中的bean对象 ({@link #registerBean} 方法无法注册Configuration中的bean)
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param clazz              注册的bean的类
     * @param args               构造方法的必要参数，顺序和类型要求和clazz中定义的一致
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerConfigurationBean(ConfigurableApplicationContext applicationContext, String beanName, Class<T> clazz, Object... args) {
        // 校验beanName是否存在
        T bean = checkBeanByBeanName(applicationContext, beanName, clazz);
        if (bean != null) {
            return bean;
        }

        // 若不包含beanName，则创建beanDefinition构建者，并按照顺序添加构造方法的参数
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        for (Object arg : args) {
            beanDefinitionBuilder.addConstructorArgValue(arg);
        }

        // 获取ConfigurationClassPostProcessor对象
        ConfigurationClassPostProcessor configurationClassPostProcessor = applicationContext.getBean(ConfigurationClassPostProcessor.class);

        // 获取beanDefinition对象并注册
        BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
        BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
        beanFactory.registerBeanDefinition(beanName, beanDefinition);
        beanDefinition.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);

        // 注册Configuration类中的bean
        configurationClassPostProcessor.enhanceConfigurationClasses(applicationContext.getBeanFactory());
        return applicationContext.getBean(beanName, clazz);
    }

    /**
     * 注册一个bean对象
     *
     * @param applicationContext sping上下文
     * @param beanName           beanName
     * @param clazz              bean类
     * @param instanceSupplier   函数式接口返回bean对象
     */
    public static <T> T registerSingleton(ConfigurableApplicationContext applicationContext, String beanName, Class<T> clazz, Supplier<T> instanceSupplier) {
        // 先按照beanName检查
        T bean = checkBeanByBeanName(applicationContext, beanName, clazz);
        if (bean == null) {
            // 注册单例的话，每个类只能有一个bean对象，因此需要再按照beanClass检查
            bean = checkBeanByBeanClass(applicationContext, clazz);
            if (bean == null) {
                DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
                bean = instanceSupplier.get();
                defaultListableBeanFactory.registerSingleton(beanName, bean);
            }
        }
        return bean;
    }

    /**
     * 注册一个bean对象
     *
     * @param applicationContext sping上下文
     * @param beanName           beanName
     * @param instanceSupplier   函数式接口返回bean对象
     */
    public static <T> void registerSingleton(ConfigurableApplicationContext applicationContext, String beanName, Supplier<T> instanceSupplier) {
        // 先按照beanName检查
        Object bean = checkBeanByBeanName(applicationContext, beanName);
        if (bean == null) {
            DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
            defaultListableBeanFactory.registerSingleton(beanName, instanceSupplier.get());
        }
    }

    /**
     * 将给定源bean的属性值复制到目标class生成的对象中，如果没有匹配的属性则返回null
     *
     * @param source           the source bean
     * @param editable         the class (or interface) to restrict property setting to
     * @param ignoreProperties array of property names to ignore
     * @throws BeansException if the copying failed
     * @see BeanWrapper
     */
    @Nullable
    public static <T> T copyOf(Object source, @NonNull Class<T> editable, @Nullable String... ignoreProperties)
            throws BeansException, IllegalAccessException, InstantiationException {

        Assert.notNull(source, "Source must not be null");

        T target = editable.newInstance();

        PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(editable);
        List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
        int matchCount = 0;
        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                try {
                    Object value = null;
                    if (source instanceof Map) {
                        Map map = (Map) source;
                        value = map.get(targetPd.getName());
                    } else {
                        PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(source.getClass(), targetPd.getName());
                        if (sourcePd != null) {
                            Method readMethod = sourcePd.getReadMethod();
                            if (readMethod != null &&
                                    ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                                if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                    readMethod.setAccessible(true);
                                }
                                value = readMethod.invoke(source);
                            }
                        }
                    }
                    if (value != null) {
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
                        value = ConverterUtil.cast(value, writeMethod.getParameterTypes()[0]);
                        writeMethod.invoke(target, value);
                        matchCount++;
                    }
                } catch (Throwable ex) {
                    throw new FatalBeanException(
                            "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                }
            }
        }
        return matchCount > 0 ? target : null;
    }

    /**
     * 根据beanName检查并获取bean对象
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @param clazz              注册的bean的类
     * @return 返回注册到容器中的bean对象
     */
    @SuppressWarnings("unchecked")
    private static <T> T checkBeanByBeanName(ConfigurableApplicationContext applicationContext, String beanName, Class<T> clazz) {
        // 先判断spring上下文中是否已经存在该beanName，如果存在判断是否和需要注册的bean的类型相同，若相同返回，若不相同则抛出异常
        Object bean;
        if (applicationContext.containsBean(beanName)) {
            bean = applicationContext.getBean(beanName);
            if (bean.getClass().isAssignableFrom(clazz)) {
                return (T) bean;
            } else {
                throw new BeanCreationNotAllowedException(beanName, "The beanName '" + beanName + "' has been created by type '" + bean.getClass().getName() + "'!");
            }
        } else {
            try {
                return applicationContext.getBean(clazz);
            } catch (BeansException e) {
                // ignore
            }
        }
        return null;
    }

    /**
     * 根据beanName检查并获取bean对象
     *
     * @param applicationContext Spring上下文
     * @param beanName           beanName
     * @return 返回注册到容器中的bean对象
     */
    private static Object checkBeanByBeanName(ConfigurableApplicationContext applicationContext, String beanName) {
        try {
            return applicationContext.getBean(beanName);
        } catch (BeansException e) {
            return null;
        }
    }

    /**
     * 根据beanClass检查并获取bean对象
     *
     * @param applicationContext Spring上下文
     * @param beanClass          注册的bean的类
     * @return 返回注册到容器中的bean对象
     */
    private static <T> T checkBeanByBeanClass(ConfigurableApplicationContext applicationContext, Class<T> beanClass) {
        try {
            return applicationContext.getBean(beanClass);
        } catch (BeansException e) {
            return null;
        }
    }

    private BeanUtil() {
    }
}